遠雨【使用说明】Obisidian Callouts脚本 2025-09-23
Obisidian Callouts脚本
油猴Obisidian Callouts脚本之二
// ==UserScript==
// @name Markdown Callout
// @namespace http://tampermonkey.net/
// @version 3.0
// @description Obsidian风格Calloutcha插件,支持输入中文关键词转换并附带工具栏按钮
// @match https://linux.do/*
// @grant none
// ==/UserScript==
(function() {
‘use strict’;
// 核心功能:定义中文别名到英文关键词的映射
const calloutAliasMap = {
'笔记': 'note',
'摘要': 'abstract', '概要': 'abstract', '总结': 'summary',
'信息': 'info',
'待办': 'todo', '任务': 'todo',
'技巧': 'tip', '提示': 'tip', '窍门': 'hint',
'重要': 'important',
'成功': 'success', '完成': 'done', '检查': 'check',
'问题': 'question', '帮助': 'help', '问答': 'faq',
'警告': 'warning', '注意': 'caution', '当心': 'attention',
'失败': 'failure', '错误': 'fail', '丢失': 'missing',
'危险': 'danger', '报错': 'error', '漏洞': 'bug',
'示例': 'example', '例子': 'example',
'引用': 'quote', '引述': 'cite',
};
// Callout类型配置,包含颜色和中文名称
const calloutTypes = [
{ type: 'note', name: '笔记', color: '#448aff' },
{ type: 'abstract', name: '摘要', color: '#00b0ff' },
{ type: 'info', name: '信息', color: '#00b8d4' },
{ type: 'todo', name: '待办', color: '#00bcd4' },
{ type: 'tip', name: '技巧', color: '#00c853' },
{ type: 'success', name: '成功', color: '#00e676' },
{ type: 'question', name: '问题', color: '#64dd17' },
{ type: 'warning', name: '警告', color: '#ff9800' },
{ type: 'failure', name: '失败', color: '#ff5722' },
{ type: 'danger', name: '危险', color: '#f44336' },
{ type: 'bug', name: '漏洞', color: '#e91e63' },
{ type: 'example', name: '示例', color: '#7c4dff' },
{ type: 'quote', name: '引用', color: '#9e9e9e' }
];
/**
* 获取当前编辑器状态的辅助函数
*/
function getEditorState(target) {
const text = target.value;
const selectionStart = target.selectionStart;
const textUpToCursor = text.substring(0, selectionStart);
const currentLineIndex = textUpToCursor.split('\n').length - 1;
const lines = text.split('\n');
return {
lines: lines,
currentLineIndex: currentLineIndex,
currentLine: lines[currentLineIndex],
lineStartIndex: textUpToCursor.lastIndexOf('\n') + 1,
};
}
/**
* 检测当前光标位置的Callout嵌套层级
*/
function detectCalloutNesting(textarea) {
const text = textarea.value;
const cursorPos = textarea.selectionStart;
const textBeforeCursor = text.substring(0, cursorPos);
const lines = textBeforeCursor.split('\n');
// 从当前行开始往上查找,找到最近的Callout上下文
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i];
const trimmedLine = line.trim();
// 如果遇到完全空行,说明嵌套被中断,重置为0
if (trimmedLine === '') {
return 0;
}
// 检查是否是Callout相关的行(标题行、内容行或空的>行)
const calloutMatch = trimmedLine.match(/^(>+)(\s*\[!|\s+.*|\s*$)/);
if (calloutMatch) {
return calloutMatch[1].length;
}
// 如果遇到非Callout行,说明不在Callout上下文中
break;
}
return 0;
}
/**
* 插入Callout样式到编辑器
*/
function insertCallout(textarea, calloutType, title = '') {
const selectionStart = textarea.selectionStart;
const selectionEnd = textarea.selectionEnd;
const selectedText = textarea.value.substring(selectionStart, selectionEnd);
if (selectedText) {
// 如果有选中文本,只在选中内容前面添加一个Callout样式
const calloutHeader = `> [!${calloutType}]${title ? ' ' + title : ''}\n> `;
const beforeText = textarea.value.substring(0, selectionStart);
const afterText = textarea.value.substring(selectionEnd);
// 检查是否需要在前面添加换行
const textBeforeCursor = textarea.value.substring(0, selectionStart);
const currentLineStart = textBeforeCursor.lastIndexOf('\n') + 1;
const currentLine = textBeforeCursor.substring(currentLineStart);
// 为选中文本的Callout样式前面总是添加一个换行
let finalCalloutText = '\n' + calloutHeader;
if (currentLine.trim() !== '' && !textBeforeCursor.endsWith('\n')) {
finalCalloutText = '\n' + finalCalloutText;
}
textarea.value = beforeText + finalCalloutText + selectedText + afterText;
// 设置光标位置到选中文本的末尾
const newCursorPos = selectionStart + finalCalloutText.length + selectedText.length;
textarea.selectionStart = textarea.selectionEnd = newCursorPos;
} else {
// 如果没有选中文本,使用原来的嵌套逻辑
// 检测当前的嵌套层级
const currentNestingLevel = detectCalloutNesting(textarea);
// 每次点击都增加一层嵌套
const nestingLevel = currentNestingLevel + 1;
const prefix = '>'.repeat(nestingLevel);
const calloutText = `${prefix} [!${calloutType}]${title ? ' ' + title : ''}\n${prefix} `;
// 检查是否需要在前面添加换行
const textBeforeCursor = textarea.value.substring(0, selectionStart);
const currentLineStart = textBeforeCursor.lastIndexOf('\n') + 1;
const currentLine = textBeforeCursor.substring(currentLineStart);
let finalCalloutText = calloutText;
if (currentLine.trim() !== '' && !textBeforeCursor.endsWith('\n')) {
finalCalloutText = '\n' + calloutText;
}
const beforeText = textarea.value.substring(0, selectionStart);
const afterText = textarea.value.substring(selectionEnd);
textarea.value = beforeText + finalCalloutText + afterText;
// 设置光标位置到内容区域末尾
const newCursorPos = selectionStart + finalCalloutText.length;
textarea.selectionStart = textarea.selectionEnd = newCursorPos;
}
// 触发input事件
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.focus();
}
/**
* 更新编辑器内容和光标位置的辅助函数
*/
function updateEditor(target, lines, lineIndex, newLine, lineStartIndex) {
lines[lineIndex] = newLine;
const newText = lines.join('\n');
target.value = newText;
target.selectionStart = target.selectionEnd = lineStartIndex + newLine.length;
// 触发 input 事件以确保页面能监听到变化
target.dispatchEvent(new Event('input', { bubbles: true }));
}
/**
* 创建工具栏按钮
*/
function createToolbarButtons() {
// 查找编辑器工具栏
const toolbar = document.querySelector('.d-editor-button-bar');
if (!toolbar) return;
// 检查是否已经添加过按钮
if (toolbar.querySelector('.callout-buttons-container')) return;
// 创建按钮容器
const container = document.createElement('div');
container.className = 'callout-buttons-container';
container.style.cssText = `
display: inline-flex;
gap: 4px;
margin-left: 8px;
align-items: center;
`;
// 为每种callout类型创建按钮
calloutTypes.forEach(callout => {
const button = document.createElement('button');
button.className = 'btn no-text btn-icon callout-btn';
button.title = `插入${callout.name} Callout`;
button.style.cssText = `
width: 20px;
height: 20px;
border-radius: 50%;
background-color: ${callout.color};
border: 2px solid #fff;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
margin: 0 1px;
cursor: pointer;
transition: transform 0.1s ease;
`;
button.addEventListener('mouseenter', () => {
button.style.transform = 'scale(1.1)';
});
button.addEventListener('mouseleave', () => {
button.style.transform = 'scale(1)';
});
button.addEventListener('click', (e) => {
e.preventDefault();
const textarea = document.querySelector('.d-editor-input');
if (textarea) {
insertCallout(textarea, callout.type);
}
});
container.appendChild(button);
});
toolbar.appendChild(container);
}
/**
* 主事件处理函数
* @param {KeyboardEvent} event
*/
function handleTabPress(event) {
if (event.key !== 'Tab' || event.target.tagName !== 'TEXTAREA') {
return;
}
const target = event.target;
const { lines, currentLineIndex, currentLine, lineStartIndex } = getEditorState(target);
// 如果当前行无效或已经是 Callout,则不执行任何操作
if (currentLine === undefined || currentLine.trim().startsWith('> [!')) {
return;
}
// 阻止 Tab 的默认行为(如切换焦点)
event.preventDefault();
const prefix = currentLine.trim().split(/[\s::]/)[0]; // 支持中文冒号
const targetKeyword = calloutAliasMap[prefix];
// 如果找到了匹配的中文关键词
if (targetKeyword) {
const restOfLine = currentLine.substring(currentLine.indexOf(prefix) + prefix.length).trim();
const title = restOfLine.startsWith(':') || restOfLine.startsWith(':')
? restOfLine.substring(1).trim()
: restOfLine;
const newLine = `> [!${targetKeyword}]${title ? ' ' + title : ''}`;
updateEditor(target, lines, currentLineIndex, newLine, lineStartIndex);
}
}
/**
* 初始化函数
*/
function init() {
// 添加工具栏按钮
createToolbarButtons();
// 监听DOM变化,以便在新的编辑器出现时添加按钮
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length > 0) {
// 延迟执行,确保DOM完全加载
setTimeout(createToolbarButtons, 100);
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// 事件监听器
document.addEventListener('keydown', handleTabPress);
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// 也在页面完全加载后再次尝试初始化
window.addEventListener('load', () => {
setTimeout(init, 500);
});
})();
这个脚本是从 搞一点油猴的 Obsidian Callouts - 开发调优 - LINUX DO修改来的
在原有功能上新增了颜色工具栏,点击即可添加样式,支持嵌套

另外也支持选中内容后点击颜色按钮来添加,会将选中内容移动到新的嵌套
